package agents.resource;

import java.util.ArrayList;
import java.util.List;

import agents.exceptions.ActionFailedException;

import agents.resource.exceptions.BookingFailedException;
import agents.resource.exceptions.CollisionException;
import domain.Car;
import domain.Clock;
import domain.Road;
import domain.Segment;
import domain.TimeInterval;

public class RoadAgent extends ResourceAgent {

	public RoadAgent(Road road, CrossRoadAgent startResource, CrossRoadAgent endResource) {
		this.road = road;
		this.startResource = startResource;
		this.endResource = endResource;
		startResource.addOutgoingResource(this);
		endResource.addIngoingResource(this);
	}
	
	private Road road;
	private CrossRoadAgent startResource;
	private CrossRoadAgent endResource;

	//verplaatst auto van agent een segment op de weg, kan ook weg verlaten uit weg naar kruispunt
	public void advance(Car car) throws ActionFailedException {
		if(isRemoved()) throw new ActionFailedException();
		int segmentCar = getTrafficInfrastructure().getSegments().indexOf(car.getPosition());
		if(hasCar(car) && !isAtEndOfResource(car)) {
			//car staat niet op het einde van de weg, moet verder
			if(getTrafficInfrastructure().getSegments().get(segmentCar + 1).getCar() == null){
				car.move(getTrafficInfrastructure().getSegments().get(segmentCar + 1));
			}
			//staat nog een auto voor u
			else
				throw new ActionFailedException();
		}
	}
	
	public void moveTo(Car car, ResourceAgent neighbour) throws ActionFailedException {
		if(isRemoved()) throw new ActionFailedException();
		if(!isAtEndOfResource(car)) throw new IllegalArgumentException();
		if(endResource == null) throw new ActionFailedException("dead end");
		try {
			//eerst proberen toe te voegen, dan pas weghalen
			endResource.addCar(car);
		} catch (CollisionException e) {
			throw new ActionFailedException(e);
		}
	}
	
	public boolean isAtEndOfResource(Car car) {
		return getTrafficInfrastructure().getSegments().indexOf(car.getPosition()) == getTrafficInfrastructure().getSegments().size()-1;
	}
	
	//geeft alle nog niet geboekte intervals terug, vanaf een bepaalde tijd
	//roadintervals hebben tijd wanneer road opgereden kan worden + wanneer hij dan verlaten kan worden 
	@Override
	public List<TimeInterval> getFreeIntervals(long startTime, Car car) {

		List<TimeInterval> result = new ArrayList<TimeInterval>();
		List<Booking> bookings = getAllBookingsNotFrom(car);
		
		long lastBookingEndTime = bookings.isEmpty() ? -1 : bookings.get(bookings.size()-1).getTimeInterval().getEndTime();
		long lastExitBookingTime = getExitResourceBookings().isEmpty() ? -1 : getExitResourceBookings().get(getExitResourceBookings().size() - 1).getExitTime();
		
		long endLastBooking = Math.max(lastBookingEndTime, lastExitBookingTime); //maximum van eindtijd bookings van auto's op resource en van auto's die nog moeten komen
		
		if(endLastBooking == -1) { //als er geen enkele booking is
			result.add(new TimeInterval(startTime));
			return result;
		}
		
		long firstTime = -1;
		
		for(long i = startTime; i <= endLastBooking; i++){
			if(canCarEnterAt(i, car) && firstTime == -1) {
				firstTime = i;
			}
			if(!canCarEnterAt(i, car) && firstTime != -1){
				result.add(new TimeInterval(firstTime, i-1));
				firstTime = -1;
			}
		}
		if(firstTime == -1) {
			result.add(new TimeInterval(Math.max(endLastBooking+1,startTime)));
		} else {
			result.add(new TimeInterval(firstTime));
		}
		
		return result;
	}
	
	//geeft het interval terug waarop de car indien hij op de entertime binnenkomt de road terug kan verlaten
	@Override
	public TimeInterval getPossibleExitInterval(long enterTime, Car car){
		List<Booking> bookings = getAllBookingsNotFrom(car);
		long noTrafficTime = enterTime + getTrafficInfrastructure().getTime() - 1;
		long lastCarOnResourceExitTime = getExitResourceBookings().isEmpty() ? -1 : getExitResourceBookings().get(getExitResourceBookings().size() - 1).getExitTime();
		long firstPossibleEndTime = Math.max(noTrafficTime, lastCarOnResourceExitTime+1);
		//geen booking
		if(bookings.size() == 0)
			return new TimeInterval(firstPossibleEndTime);
		//voor eerste booking
		if(enterTime < bookings.get(0).getTimeInterval().getStartTime()) {
			if(firstPossibleEndTime < bookings.get(0).getTimeInterval().getEndTime()) 
				return new TimeInterval(firstPossibleEndTime, bookings.get(0).getTimeInterval().getEndTime()-1);
			else
				return null;
		}
		//na laatste booking
		if(enterTime > bookings.get(bookings.size()-1).getTimeInterval().getStartTime()){
			return new TimeInterval(Math.max( firstPossibleEndTime, bookings.get(bookings.size()-1).getTimeInterval().getEndTime()+1 ));
		}
		// tussen 2 bookings
		for(int i = 0; i< bookings.size() -1; i++){
			//long bookingStartTime = bookings.get(i).getTimeInterval().getStartTime();
			long bookingEndTime = bookings.get(i).getTimeInterval().getEndTime();
			long nextBookingStartTime = bookings.get(i+1).getTimeInterval().getStartTime();
			long nextBookingEndTime = bookings.get(i+1).getTimeInterval().getEndTime();
			
			if(enterTime <= nextBookingStartTime) { //als enterTime tussen booking en nextBooking ligt
				if(enterTime == nextBookingStartTime ||  //enterTime mag niet bezet zijn
				   nextBookingEndTime <= firstPossibleEndTime || //moet weg zijn voor einde van nextBooking
				   bookingEndTime ==  nextBookingEndTime -1) {  //moet 1 tijdseenheid plaats zijn tussen 2 bookings om weg te gaan
					return null;
				}
				else //komt binnen tussen booking en nextBooking
					return new TimeInterval( Math.max(bookingEndTime+1, firstPossibleEndTime), nextBookingEndTime-1 );
			}
		}
		return null;
	}
	
	//geeft true terug indien op dat moment de road vol staat
	private boolean canCarEnterAt(long time,Car car){
		List<Booking> bookings = getAllBookingsNotFrom(car);
		
		for(Booking book :bookings) {
			if(book.getTimeInterval().getStartTime() == time) return false;
		}
		
		int carCount = 0;
		for(Booking book : bookings){
			if(book.getTimeInterval().contains(time)){
				carCount++;
			}
		}
		
		for(ExitResourceBooking book : getExitResourceBookings()) {
			if(book.getExitTime() >= time) {
				carCount++;
			}
		}
		
		return carCount < getTrafficInfrastructure().getLength();
	}

	@Override
	public Road getTrafficInfrastructure() {
		return road;
	}
	
	@Override
	public List<ResourceAgent> getIngoingResources() {
		ArrayList<ResourceAgent> l = new ArrayList<ResourceAgent>();
		if(startResource != null) l.add(startResource);
		return l;
	}

	@Override
	public List<ResourceAgent> getOutgoingResources() {
		ArrayList<ResourceAgent> l = new ArrayList<ResourceAgent>();
		if(endResource != null) l.add(endResource);
		return l;
	}

	//	geeft interval terug wanneer car eraf kan tot wanneer hij er af moet (van car al op de road)
	@Override
	public TimeInterval getDesiredExitTime(Car car) {
		long now = Clock.getInstance().getCurrentTime();
		long minTravelTime = getTrafficInfrastructure().getTravelTimeFrom(car.getPosition());
		long carBeforeExitTime = getExitTimeOfCarBefore(car); //-1 als er gene is
		
		long intervalStart;
		if(carBeforeExitTime == -1) {
			intervalStart = now + minTravelTime;
		} else {
			intervalStart = Math.max(carBeforeExitTime+1, now+minTravelTime);
		}
		
		long carAfterExitTime = getExitTimeOfCarAfter(car); //-1 wanneer er gene is
		if(carAfterExitTime == -1) {
			return new TimeInterval(intervalStart);
		}
		return new TimeInterval(intervalStart, Math.max(carAfterExitTime-1,intervalStart));
	}
	
	private long getExitTimeOfCarAfter(Car car) {
		List<Booking> bookings = getAllBookings();
		List<Car> carsToFilter = getAllCarsBefore(car);
		carsToFilter.add(car);
		
		for(Car c : carsToFilter) {
			bookings.remove(getBookingFromCar(c));
		}
		
		if(bookings.size() == 0) return -1;
		
		return bookings.get(0).getTimeInterval().getEndTime() - 1;
	}

	//geeft een lijst van alle auto's voor opgegeven auto (dus dichterbij einde van de weg)
	//in volgorde met auto juist voor gegeven auto eerst in lijst
	private List<Car> getAllCarsBefore(Car car) {
		List<Segment> segments = getTrafficInfrastructure().getSegments();
		List<Car> carsBefore = new ArrayList<Car>();
		
		int mySegment = segments.indexOf(car.getPosition());
		for(int i = mySegment+1; i < segments.size(); i++) {
			Car c = segments.get(i).getCar();
			if(c!=null) {
				carsBefore.add(c);
			}
		}
		return carsBefore;
	}

	private long getExitTimeOfCarBefore(Car car) {
		List<Segment> segments = getTrafficInfrastructure().getSegments();
		int mySegment = segments.indexOf(car.getPosition());
		for(int i = mySegment+1; i < segments.size(); i++) {
			Car c = segments.get(i).getCar();
			if(c!=null) {
				Booking b = findBookingOf(c);
				if(b != null) {
					return b.getTimeInterval().getEndTime();
				}
			}
		}
		return -1;
	}

	private Booking findBookingOf(Car c) {
		List<Booking> bookings = getAllBookings();
		for(Booking b : bookings) {
			if(b.getCar() == c) {
				return b;
			}
		}
		return null;
	}

	@Override
	public void addCar(Car car) throws CollisionException {
		if(getTrafficInfrastructure().getSegments().get(0).getCar() != null)
			throw new CollisionException();
		else
			car.move(getTrafficInfrastructure().getSegments().get(0));
	}

	@Override
	void addIngoingResource(ResourceAgent in) {
		if(startResource != null) throw new IllegalStateException();
		if(!(in instanceof CrossRoadAgent)) throw new IllegalArgumentException();
		
		startResource = (CrossRoadAgent) in;
	}

	@Override
	void addOutgoingResource(ResourceAgent out) {
		if(endResource != null) throw new IllegalStateException();
		if(!(out instanceof CrossRoadAgent)) throw new IllegalArgumentException();
		
		endResource = (CrossRoadAgent) out;
	}

	@Override
	void removeIngoingResource(ResourceAgent in) {
		if(in == startResource) startResource = null;
	}

	@Override
	void removeOutgoingResource(ResourceAgent out) {
		if(out == endResource) endResource = null;		
	}

	@Override
	public long getNoTrafficExitTime(Car car) {
		return Clock.getInstance().getCurrentTime() - 1 + getTrafficInfrastructure().getTravelTimeFrom(car.getPosition());
	}

	//gooit bookingfailedexception als booking gebeurt voor vorige auto
	@Override
	public void bookExitResource(long exitTime, Car car) throws BookingFailedException {
		if(exitTime < getFirstExitTimeOf(car)) throw new BookingFailedException("kan er op gegeven tijd nog niet afgeraken");

		ExitResourceBooking b = getExitResourceBookingOf(car);
		
		if(b != null) { // als car zijn exit time al geboekt heeft, vervang deze
			b.setExitTime(exitTime);
		} else { //anders vind de index waar deze toegevoegd moet worden;
			ExitResourceBooking newBooking = new ExitResourceBooking(car,exitTime);
			
			int indexToAdd = 0;
			List<Car> carsBefore = getAllCarsBefore(car);
			for(Car carBefore : carsBefore) {
				ExitResourceBooking bookingBefore = getExitResourceBookingOf(carBefore);
				if(bookingBefore != null) {
					indexToAdd = getExitResourceBookings().indexOf(bookingBefore)+1;
					break;
				}
			}
			
			getExitResourceBookings().add(indexToAdd, newBooking);
		}
		
		
		shiftConflictingExitTimes();
		shiftConflictingBookings();
	}

	//schuif alle conflicterende exit bookings op
	private void shiftConflictingExitTimes() {
		List<ExitResourceBooking> exitBookings = new ArrayList<ExitResourceBooking>(getExitResourceBookings());
		long minNextExitTime = Clock.getInstance().getCurrentTime()-1;

		for(ExitResourceBooking b : exitBookings) {
			if(minNextExitTime > b.getExitTime()) { //booking aanpassen als deze conflicteert met vorige last exit time
				b.setExitTime(minNextExitTime);
			} 
			minNextExitTime = b.getExitTime() + 1;
		}		
	}

	//eerste exit time voor auto op weg, auto's voor hem in rekening gebracht
	public long getFirstExitTimeOf(Car car) {
		List<Car> carsBefore = getAllCarsBefore(car);

		long distToCarBefore = 1;
		for(Car carBefore : carsBefore) {
			ExitResourceBooking exitBookingBefore = getExitResourceBookingOf(carBefore);
			if(exitBookingBefore != null) {
				return Math.max(getNoTrafficExitTime(car), exitBookingBefore.getExitTime()+distToCarBefore); 
			}
			distToCarBefore++;
		}
		
		return getNoTrafficExitTime(car);
	}
	
	private ExitResourceBooking getExitResourceBookingOf(Car car) {
		for(ExitResourceBooking b : getExitResourceBookings()) {
			if(b.getCar() == car) return b;
		}
		return null;
	}

	@Override
	protected long getLastBookedExitTime() {
		if(getExitResourceBookings().isEmpty()) return Clock.getInstance().getCurrentTime()-1;
		ExitResourceBooking b = getExitResourceBookings().get(getExitResourceBookings().size()-1);
		return b.getExitTime();
	}
	
	private final List<ExitResourceBooking> exitResourceBookings = new ArrayList<ExitResourceBooking>();
	
	private class ExitResourceBooking {
		private final Car car;
		private long exitTime;
		
		public ExitResourceBooking(Car car, long exitTime) {
			this.car = car;
			this.exitTime = exitTime;
		}

		public Car getCar() {
			return car;
		}

		public long getExitTime() {
			return exitTime;
		}
		
		public void setExitTime(long newExitTime) {
			this.exitTime = newExitTime;
		}
		
		public String toString() {
			return getCar().toString() + " at " + getExitTime();
		}
	}
	
	//schuif alle bookings op totdat deze niet meer conflicteren met de exit times van auto's op de resource
	protected void shiftConflictingBookings() {
		List<Booking> bookingsBefore = new ArrayList<Booking>();
		for(Booking b : getAllBookings()) {
			updateTime(b,bookingsBefore);
			bookingsBefore.add(b);
		}
		
	}

	private void updateTime(Booking b, List<Booking> bookingsBefore) {
		long enterTime = getFirstPossibleEntryTimeAfter(b.getTimeInterval().getStartTime(),bookingsBefore);
		
		long firstPossibleExitTime; //eerst mogelijke exit time rekening houdend met auto's in bookings voor deze booking
		if(bookingsBefore.isEmpty()) {
			if(hasCars())
				firstPossibleExitTime = getLastBookedExitTime() + 1;
			else
				firstPossibleExitTime = Clock.getInstance().getCurrentTime();
		} else {
			firstPossibleExitTime = bookingsBefore.get(bookingsBefore.size() - 1).getTimeInterval().getEndTime() + 1;
		}
		
		long exitTime = Math.max(firstPossibleExitTime, enterTime + getTrafficInfrastructure().getTime()-1);
		exitTime = Math.max(exitTime, b.getTimeInterval().getEndTime());
		
		TimeInterval newBookingTime = new TimeInterval(enterTime,exitTime);
		b.setTimeInterval(newBookingTime);
	}
	
	private long getFirstPossibleEntryTimeAfter(long time, List<Booking> bookings) {
		long minPossible;
		if(bookings.isEmpty()) {
			minPossible = time;
		} else { 
			minPossible = Math.max(time,bookings.get(bookings.size() - 1).getTimeInterval().getStartTime() + 1);
		}
		
		while(true) {
			int carCount = 0;
			for(ExitResourceBooking b : getExitResourceBookings()) {
				if(b.getExitTime() >= minPossible) carCount++;
			}
			
			for(Booking b : bookings) {
				if(b.getTimeInterval().contains(minPossible)) carCount++;
			}
			if(carCount < getTrafficInfrastructure().getLength()) return minPossible;
			minPossible++; //indien weg vol is op minPossible, probeer volgend tijdstip
		}
	}

	//geeft alle exit resource bookings terug; deze bookings zijn alleen van auto's die zich nu op de resource bevinden 
	private List<ExitResourceBooking> getExitResourceBookings() {
		List<ExitResourceBooking> erbs = new ArrayList<ExitResourceBooking>(exitResourceBookings);
		for(ExitResourceBooking b : erbs) {
			if(!hasCar(b.getCar())) exitResourceBookings.remove(b);
		}
		return exitResourceBookings;
	}

	@Override
	protected boolean hasExitBooking(Car car) {
		return getExitResourceBookingOf(car) != null;
	}

}